package cn.crane4j.core.support.container.query;

import cn.crane4j.annotation.MappingType;
import cn.crane4j.core.support.container.MethodInvokerContainerCreator;
import cn.crane4j.core.util.Asserts;
import cn.crane4j.core.util.CollectionUtils;
import cn.crane4j.core.util.StringUtils;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

/**
 * A standard implementation of {@link AbstractQueryContainerProvider}.
 * provides the ability that generate query container by the specified namespace
 * and generate namespace by the specified query information.
 *
 * @author huangchengxing
 * @see AbstractQueryContainerProvider
 */
@Slf4j
public abstract class NamespaceResolvableQueryContainerProvider<T> extends AbstractQueryContainerProvider<T> {

    /**
     * the template of namespace, such as {@code @ProviderClassSimpleName#repository#keyProperty#prop1,prop2,prop3...}.
     */
    protected static final String NAMESPACE_TEMPLATE = "@{}#{}#{}#{}";
    protected final Map<String, MappingType> mappingTypes = new ConcurrentHashMap<>(8);
    protected static final String NULL = "NULL";

    /**
     * Constructor.
     *
     * @param methodInvokerContainerCreator {@link MethodInvokerContainerCreator} instance.
     */
    protected NamespaceResolvableQueryContainerProvider(MethodInvokerContainerCreator methodInvokerContainerCreator) {
        super(methodInvokerContainerCreator);
    }

    /**
     * Set query mapping type.
     *
     * @param namespace namespace
     * @param mappingType mapping type
     */
    public void setMappingType(String namespace, MappingType mappingType) {
        mappingTypes.put(namespace, mappingType);
    }

    /**
     * Determines the namespace of container.
     * Such as {@code @classSimpleName#name#key#xxx,xxx,xxx}.
     *
     * @param name        mapper name
     * @param keyProperty key field name for query, if it is empty, it defaults to the specified key field
     * @param properties  fields to query, if it is empty, all table columns will be queried by default.
     * @return namespace
     * @see #resolveQueryInfo(String)
     */
    @Override
    public String determineNamespace(String name, @Nullable String keyProperty, @Nullable List<String> properties) {
        Asserts.isNotNull(name, "name must not be null");
        String prefix = getClass().getSimpleName();
        String key = StringUtils.isEmpty(keyProperty) ? NULL : keyProperty;
        String props = CollectionUtils.isEmpty(properties) ? NULL : String.join(",", properties);
        return StringUtils.format(NAMESPACE_TEMPLATE, prefix, name, key, props);
    }

    /**
     * Resolves the query information for the specified namespace which generated by {@link #determineNamespace}.
     *
     * @param namespace namespace.
     * @return {@link QueryInfo} instance.
     */
    @Override
    protected QueryInfo resolveQueryInfo(String namespace) {
        String[] parts = namespace.split("#");
        if (parts.length != 4) {
            log.warn("cannot resolve query info for namespace: {}", namespace);
            return null;
        }
        // ignore the prefix
        String name = parts[1];
        String key = StringUtils.isEmpty(parts[2]) || Objects.equals(parts[2], NULL) ? null : parts[2];
        List<String> props = StringUtils.isEmpty(parts[3]) || Objects.equals(parts[3], NULL) ?
                null : Arrays.asList(parts[3].split(","));
        QueryInfoImpl queryInfo = new QueryInfoImpl(name, key, props);
        MappingType mappingType = mappingTypes.get(namespace);
        if (Objects.nonNull(mappingType)) {
            queryInfo.setMappingType(mappingType);
        }
        return queryInfo;
    }

    /**
     * Query information.
     *
     * @author huangchengxing
     */
    @Getter
    @RequiredArgsConstructor
    protected static class QueryInfoImpl implements QueryInfo {
        private final String repository;
        @Nullable
        private final String keyProperty;
        @Nullable
        private final List<String> properties;
        @Setter
        private MappingType mappingType = MappingType.ONE_TO_ONE;
    }
}
